/*

This software is licensed under the terms of the MIT licence.

Copyright (c) 2009 Mihaly Heder

Permission is hereby granted, free of charge, to any person obtaining a copy of 
this software and associated documentation files (the "Software"), to deal in the 
Software without restriction, including without limitation the rights to use, copy, 
modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, 
and to permit persons to whom the Software is furnished to do so, subject to the 
following conditions:

The above copyright notice and this permission notice shall be included in all copies 
or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR 
PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE 
FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 
SOFTWARE.

*/

package jsentence;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.io.UnsupportedEncodingException;

import java.io.IOException;
import java.io.OutputStreamWriter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Hashtable;
import java.util.List;
import java.util.Scanner;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPathExpressionException;

import org.w3c.dom.Document;
import org.xml.sax.SAXException;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import info.mihaly.heder.xmlutils.XMLHelper;
import org.w3c.dom.NodeList;

public class JSentence {

	final Logger logger = LoggerFactory.getLogger(JSentence.class);

	String mode;
	String breaker;
	String delimiterRegex;
	Collection<Rule> rules;
	Hashtable<String, Collection<String>> abbreviations;

	List<Integer> indexes;

	// TODO
	double base;
	double passAbove;

	private File inFile;

	private String inputSource = "";

	private String inputString = "";

	private String outDestination = "";

	private String charsetName ;

	private File outFile;

	private OutputStreamWriter fw;
	
	private InputStream inputStream;

	private OutputStream outputStream;
	
	private String outString;
	
	private int lenghtCounter = 0;
	
	private int sentenceCounter = 0;

	private double applyWeight(double b, double w) {
		logger.debug("Score change " + mode + " " + b + " by " + w);
		if (mode.equals("additive")) {
			return b + w;
		}
		if (mode.equals("multiplicative")) {
			return b * w;
		}
		return 0.0;
	}

	private String getInputText() {
		StringBuffer sb = new StringBuffer();
		Scanner sc;
		try {
			if (inputSource.equals("File")) {
				sc = new Scanner(inFile, charsetName);
				logger.debug("Opened input file" + inFile.getAbsolutePath());
			} else if (inputSource.equals("String")) {
				return inputString;
			}
			else {
				sc = new Scanner(inputStream, charsetName);
			}
			while (sc.hasNextLine()) {
				logger.trace("Reading line from input...");
				sb.append(sc.nextLine());
				sb.append(' ');
			}
		} catch (FileNotFoundException e) {
			logger.error("File Not Found:" + inFile.getAbsolutePath());
			System.exit(1);
		}
		return sb.toString();
	}

	private void outputSentence(String s) {
		lenghtCounter+= s.length();
		indexes.add(lenghtCounter);
		sentenceCounter++;
		logger.debug("writing sentence to output:" + s);
		logger.debug("lenghtCounter:" + lenghtCounter + " sentenceCounter:"+sentenceCounter);
		try {
			if (! outDestination.equals("None")) {
				fw.write(s);
				fw.write('\n');
			}
		} catch (IOException e) {
			if (outDestination.equals("File")) {
				logger.error("Cannot write to file"
								+ outFile.getAbsolutePath());
				e.printStackTrace();
			} else {
				logger.error("Cannot write to stdout");
				// though it's not likely
			}
		}
	}

	
	/**
	 * Initializes the JSentence object.
	 * 
	 * Initializes the Jsentence object with the given path of the config file, and the given character encoding.
	 * 
	 * @param configFile
	 * @param charsetName
	 */
	public JSentence(String configFile,String charsetName) {
	/*	this.inputSource = inputSource;
		this.inputString = inputString;
		this.inFile = inFile;
		this.outDestination = outDestination;
		this.outFile = outFile;
		
		if (inputSource.equals("File")) {
			logger.debug("Using " + this.inputSource + " input :" + inFile.getAbsolutePath());
		}
		if (outDestination.equals("File")) {
			logger.debug("Using " + this.outDestination + " output :" + outFile.getAbsolutePath());
		} */

		this.charsetName = charsetName;
		
		indexes = new ArrayList<Integer>();
		
		try {
			rules = new ArrayList<Rule>();
			abbreviations = new Hashtable<String, Collection<String>>();

			logger.debug("Parsing configuration file:" + configFile);
			Document configDoc = XMLHelper.getDocumentFromXmlFile(new File(
					configFile));

			
			breaker = XMLHelper.getStringByXPath(configDoc,
				"/jsentence-config/@breaker");
			// mode
			mode = XMLHelper.getStringByXPath(configDoc,
					"/jsentence-config/@mode");
			if (!mode.equals("additive") && !mode.equals("multiplivative")) {
				logger
						.error("mode must be either 'additive' or 'multiplivative'");
				System.exit(1);
			}
			logger.debug("Configuring operation mode:" + mode);

			// base
			base = Double.parseDouble(XMLHelper.getStringByXPath(configDoc,
					"/jsentence-config/@base"));
			logger.debug("Base for each chunk:" + base);

			// passabove
			passAbove = Double.parseDouble(XMLHelper.getStringByXPath(
					configDoc, "/jsentence-config/@passAbove"));
			logger.debug("Pass above:" + passAbove);

			// delimiters
			delimiterRegex = "";
			NodeList delimnl = XMLHelper.getNodeListByXPath(configDoc,
					"/jsentence-config/delimiters/delimiter");
			for (int i = 0; i < delimnl.getLength(); i++) {
				String delim = delimnl.item(i).getTextContent();
				logger.debug("Adding delimiter:" + delim);
				delimiterRegex += delim;
			}
			logger.debug("Delimiter regex is:" + delimiterRegex);

			// abbreviations
			NodeList abbrevnl = XMLHelper
					.getNodeListByXPath(configDoc,
							"/jsentence-config/abbreviation-sets/abbreviation-set-definition");
			for (int i = 0; i < abbrevnl.getLength(); i++) {
				String name = abbrevnl.item(i).getAttributes().getNamedItem(
						"name").getNodeValue();
				logger.debug("Adding abbreviation-set-definition:" + name);
				logger.debug("Processing abbreviations...");
				NodeList children = abbrevnl.item(i).getChildNodes();
				Collection<String> abbrevl = new ArrayList<String>();
				for (int j = 0; j < children.getLength(); j++) {
					if (children.item(j).getNodeName().equals("abbreviation")) {
						String abbreviation = children.item(j).getTextContent();
						logger.debug("Found abbrebiation:" + abbreviation);
						abbrevl.add(abbreviation);
					}
				}

				abbreviations.put(name, abbrevl);

			}

			// rules
			NodeList rulenl = XMLHelper.getNodeListByXPath(configDoc,
					"/jsentence-config/rules/rule");
			for (int i = 0; i < rulenl.getLength(); i++) {
				String name = rulenl.item(i).getAttributes().getNamedItem(
						"name").getNodeValue();
				logger.debug("Adding rule:" + name);
				double weight = Double.parseDouble(rulenl.item(i)
						.getAttributes().getNamedItem("weight").getNodeValue());
				logger.debug("Rule weight:" + weight);
				Rule r = new Rule(name, weight);
				logger.debug("Processing RuleParts...");
				NodeList children = rulenl.item(i).getChildNodes();
				for (int j = 0; j < children.getLength(); j++) {
					if (children.item(j).getNodeName().equals("rulepart")) {
						int position = Integer.parseInt(children.item(j)
								.getAttributes().getNamedItem("position")
								.getNodeValue());
						NodeList grandChildren = children.item(j)
								.getChildNodes();
						for (int k = 0; k < grandChildren.getLength(); k++) {
							if (grandChildren.item(k).getNodeName().equals(
									"regex")) {
								String regex = grandChildren.item(k)
										.getTextContent();
								
								regex = regex.replaceAll("&lt;", "<");
								regex = regex.replaceAll("&gt;", ">");
								
								logger
										.debug("Configuring regex rulepart. Position:"
												+ position + " regex:" + regex);
								RegexRulePart rrp = new RegexRulePart(position,
										regex);
								r.addRulePart(rrp);
								logger.debug("RulePart added to rule");
							}
							if (grandChildren.item(k).getNodeName().equals(
									"abbreviation-set")) {
								String refname = grandChildren.item(k)
										.getAttributes().getNamedItem("name")
										.getNodeValue();
								logger
										.debug("Configuring abbreviation rulepart. Position:"
												+ position + " name:" + refname);
								if (!abbreviations.containsKey(refname)) {
									logger.error("abbreviation-set '" + refname
											+ "' not defined!");
									System.exit(1);
								}
								AbbreviationRulePart arp = new AbbreviationRulePart(
										position, abbreviations.get(refname));
								r.addRulePart(arp);
								logger.debug("RulePart added to rule");
							}

						}
					}
				}
				rules.add(r);
			}

			logger.debug("Finished parsing configuration file");
			logger.debug("----");

		} catch (ParserConfigurationException e) {
			e.printStackTrace();
		} catch (SAXException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		} catch (XPathExpressionException e) {
			e.printStackTrace();
		}

	}

	/**
	 * Executes the algorithm on the given file.
	 * 
	 * Executes the sentence end finder algorithm on the given file. No output is written.
	 * Use this if you only need the indices of the sentence ends.
	 * 
	 * @param inFile
	 */
	public void execute(File inFile) {
		this.inputSource = "File";
		this.inFile = inFile;
		this.outDestination = "None";
		logger.debug("Using " + this.inputSource + " input :" + inFile.getAbsolutePath());
		logger.debug("Using " + this.outDestination + " output :" + outFile.getAbsolutePath());
		execute();
	}

	/**
	 * Executes the algorithm on the given String.
	 * 
	 * Executes the sentence end finder algorithm on the given String. No output is written.
	 * Use this if you only need the indices of the sentence ends.
	 * 
	 * @param inFile
	 */
	public void execute(String inString) {
		this.inputSource = "String";
		this.inputString = inString;
		this.outDestination = "None";
		logger.debug("Using " + this.inputSource + " input");
		logger.debug("Using " + this.outDestination + " output");
		execute();
	}

	/**
	 * Executes the algorithm on the given String.
	 * 
	 * Executes the sentence end finder algorithm on the given String. Output is written into the given String, one recognized sentence in each line.
	 * Optionally you can get the output by using the getOutString method after executing.
	 * 
	 * @param inFile
	 */
	//TODO! outString does not work
	public void execute(String inString,String outString) {
		this.inputSource = "String";
		this.inputString = inString;
		this.outDestination = "String";
		this.outString= outString;
		logger.debug("Using " + this.inputSource + " input");
		logger.debug("Using " + this.outDestination + " output");
		execute();
	}
	
	/**
	 * Executes the algorithm on the given File.
	 * 
	 * Executes the sentence end finder algorithm on the given File. Output is written into the given File, one recognized sentence in each line.
	 * 
	 * @param inFile
	 */
	public void execute(File inFile,File outFile) {
		this.inputSource = "File";
		this.inFile = inFile;
		this.outDestination = "File";
		this.outFile = outFile;
		logger.debug("Using " + this.inputSource + " input :" + inFile.getAbsolutePath());
		logger.debug("Using " + this.outDestination + " output :" + outFile.getAbsolutePath());
		execute();
	}

	/**
	 * Executes the algorithm on the given InputStream.
	 * 
	 * Executes the sentence end finder algorithm on the given InputSream. Output is written into the given InputSream, one recognized sentence in each line.
	 * 
	 * @param inFile
	 */
	public void execute(InputStream inputStream,OutputStream outputStream) {
		this.inputSource = "InputStream";
		this.inputStream = inputStream;
		this.outDestination = "OutputStream";
		this.outputStream = outputStream;
		logger.debug("Using " + this.inputSource + " input");
		logger.debug("Using " + this.outDestination + " output");
		execute();
	}

	/**
	 * Executes the algorithm on the given File.
	 * 
	 * Executes the sentence end finder algorithm on the given File. Output is written into the given OutputStream, one recognized sentence in each line.
	 * 
	 * @param inFile
	 */
	public void execute(File inFile, PrintStream outputStream) {
		this.inputSource = "File";
		this.inFile = inFile;
		this.outDestination = "OutputStream";
		this.outputStream = outputStream;
		logger.debug("Using " + this.inputSource + " input :" + inFile.getAbsolutePath());
		logger.debug("Using " + this.outDestination + " output");
		execute();
	}
	
	private void execute() {
		logger.debug("Starting processing input...");

		ByteArrayOutputStream baos = null;
		
		if (outDestination.equals("File")) {
			try {
				fw = new OutputStreamWriter(new FileOutputStream(outFile),
						charsetName);
			} catch (IOException e) {
				logger.error("Cannot write to file:"
						+ outFile.getAbsolutePath());
				e.printStackTrace();
			}
		} else 
		if (outDestination.equals("String")) {
			try {
				logger.debug("Using ByteOutputStream for String output");
				baos = new ByteArrayOutputStream();
				fw = new OutputStreamWriter(baos,charsetName);
			} catch (UnsupportedEncodingException e) {
				e.printStackTrace();
			}
		} 
		else if (outDestination.equals("OutputStream")) {
			try {
				logger.debug("Setting outputstream to:" + outputStream);
				fw = new OutputStreamWriter(outputStream, charsetName);
			} catch (UnsupportedEncodingException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}

		String inputText = getInputText();

		List<String> res= processString(inputText);
		
		for(String s:res) {
			outputSentence(s);
		}
		
		try {
			if (!outDestination.equals("None") && !outDestination.equals("String")) {
				fw.close();
			} else if (outDestination.equals("String")) {
				fw.close();
				logger.debug("Result Written into outString");
				outString = baos.toString(charsetName);
				logger.debug(outString);
			}
		} catch (IOException e) {
			logger.error("Cannot close file" + outFile.getAbsolutePath());
			e.printStackTrace();
		}

	}
	
	
	
	public List<String> processString(String inputText) {
		List<String> result = new ArrayList<String>();
		logger.debug("-------The full input is as follows-------");
		logger.debug(inputText);
		logger.debug("------");

		String splitterRegex = "(" + breaker + ")|([^" + delimiterRegex + breaker + "]*"+breaker+")|([^" + delimiterRegex + "]*[" + delimiterRegex
				+ "])|([^" + delimiterRegex + "]*$)";
		logger.debug("Splitter regex is:" + splitterRegex);
		Pattern pattern = Pattern.compile(splitterRegex);
		Matcher matcher = pattern.matcher(inputText);

		List<String> chunks = new ArrayList<String>();

		while (!matcher.hitEnd()) {
			matcher.find();
			String chunk = matcher.group();
			if (!chunk.endsWith(breaker) || chunk.length() < 3 ) {
				logger.debug("Adding chunk:" + chunk);
				chunks.add(chunk);
			}
			else {
				String newChunk = chunk.substring(0,chunk.length() -2);
				logger.debug("Adding chunk:" + newChunk);
				chunks.add(newChunk);
				logger.debug("Adding chunk:" + breaker);
				chunks.add(breaker);
			}
		}
		
		//System.exit(0);
		
		StringBuffer ts = new StringBuffer();
		ts.append(chunks.get(0));
		int pos = 0;
		for (int i = 0; i < chunks.size() - 1; i++) {
			double score = base;
			for (Rule r : rules) {
				if (r.testADelimiter(chunks, i)) {
					score = applyWeight(score, r.getWeight());
				}
			}
			logger.info("### Chunk processed. Score: " + score);
			if ((score >= passAbove) || 
					(chunks.get(i).contains(breaker) && (i + 1 < chunks.size()) && !(chunks.get(i + 1).contains(breaker)))) {
				result.add(ts.toString());

				//--
				//pos=pos+ts.length();
				//System.out.println("Char index:"+pos);
				//--
				ts = new StringBuffer();

			}
			if (i + 1 < chunks.size()) {
				if (chunks.get(i + 1).contains(breaker)) {
					result.add(ts.toString());
					ts = new StringBuffer();					
				}
				ts.append(chunks.get(i + 1));
			}
		}
		result.add(ts.toString());
		
		return result;
	}
	
	
	
	/**
	 * Returns the list a list of integers.  
	 * 
	 * Each value marks the end of a sentence in the input.
	 * 
	 * @return
	 */
	public List<Integer> getIndexes() {
		return indexes;
	}
	
	/**
	 * Returns the recognized Sentences in a String after calling execute(...). Each line contains one recognized Sentence.
	 * 
	 * @return
	 */
	public String getOutString() {
		return outString;
	}

	
}
